package org.microsleep;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.microsleep.annotation.Cascade;
import org.microsleep.annotation.Fetch;
import org.microsleep.annotation.Id;
import org.microsleep.annotation.Table;

public abstract class ORMUtil {
	protected final Map<Class<?>, PersistentClass> mappedClasses = new HashMap<Class<?>, PersistentClass>();
	private final Map<Class<?>, Type> registeredTypes = new HashMap<Class<?>, Type>();

	protected abstract String getCreateTableStatement(PersistentClass mapping);

	protected abstract String getStoreStatement(PersistentClass pClass,
			Collection<Property> properties);

	protected abstract String getLoadStatement(PersistentClass pClass,
			Collection<Property> properties);

	public void setTypeFor(Class<?> javaClass, Type type) {
		Util.notNull(javaClass);
		registeredTypes.put(javaClass, type);
	}

	public Type getTypeFor(Class<?> javaClass) {
		Util.notNull(javaClass);
		return registeredTypes.get(javaClass);
	}

	public PersistentClass getMappingFor(Class<?> c) {
		Util.notNull(c);
		PersistentClass pClass = mappedClasses.get(c);
		if (pClass == null) {

			try {
				c.getConstructor(new Class[0]);
			} catch (SecurityException e) {
				throw new RuntimeException(e);
			} catch (NoSuchMethodException e) {
				throw new IllegalArgumentException(
						"Parameterless constructor required on " + c.getName(),
						e);
			} catch (NullPointerException e) {
				// Seems to be an android bug resulting in a NPE
				throw new IllegalArgumentException(
						"Parameterless constructor required on " + c.getName(),
						e);
			}

			pClass = new PersistentClass();
			pClass.setMappedClass(c);
			String tableName = c.getAnnotation(Table.class).name();
			if (tableName.length() == 0) {
				tableName = c.getSimpleName();
			}
			pClass.setTableName(tableName);

			mappedClasses.put(c, pClass);
			setTypeFor(c, pClass);

			try {
				List<Property> columns = new LinkedList<Property>();
				for (Method method : c.getDeclaredMethods()) {
					if (method.getName().startsWith("get")
							&& method.getParameterTypes().length == 0) {
						Property property = new Property();
						Id id = method.getAnnotation(Id.class);
						org.microsleep.annotation.Column coldef = method
								.getAnnotation(org.microsleep.annotation.Column.class);
						Util.isTrue(
								id == null || coldef != null,
								"Missing column definition for "
										+ method.getName());
						if (id != null) {
							if (pClass.getIdentifier() != null) {
								throw new IllegalArgumentException(c.getName()
										+ " has multiple ID elements!");
							}
							pClass.setIdentifier(property);
						}
						if (coldef != null) {
							columns.add(property);
							property.setName(!"".equals(coldef.name()) ? coldef
									.name() : method.getName().substring(3));
							property.setLength(coldef.length());
							property.setGetter(method);
							try {
								property.setSetter(c.getMethod(
										"set" + property.getName(),
										method.getReturnType()));
							} catch (SecurityException e) {
								throw new RuntimeException(e);
							} catch (NoSuchMethodException e) {
								throw new IllegalArgumentException(
										"Persistent property "
												+ property.getName()
												+ " requires accessible setter",
										e);
							}
							Type type = getType(property,
									method.getReturnType());
							Util.isTrue(id == null
									|| !(type instanceof PersistentClass),
									"Id must be simple type");
							property.setType(type);
							property.setJavaType(method.getReturnType());

							Fetch fetch = method.getAnnotation(Fetch.class);
							if (fetch == null
									&& type instanceof PersistentClass) {
								property.setFetchMode(FetchMode.SEPARATE);
							} else if (fetch != null) {
								Util.isTrue(type instanceof PersistentClass,
										"Fetchmode "
												+ fetch.mode()
												+ " not supported for "
												+ type.getClass().getName()
												+ " on "
												+ pClass.getMappedClass()
														.getName() + "."
												+ property.getName());
								property.setFetchMode(fetch.mode());
							}
							Cascade cascade = method
									.getAnnotation(Cascade.class);
							EnumSet<CascadeOperation> cascades = EnumSet
									.noneOf(CascadeOperation.class);
							if (cascade != null) {
								cascades.addAll(Arrays.asList(cascade.value()));
							}
							property.setCascades(cascades);
						}
					}
				}
				pClass.setColumns(columns);

				pClass.validate();

			} catch (Exception e) {
				// Small man's rollback
				mappedClasses.remove(c);
				registeredTypes.remove(c);
				throw new RuntimeException(e);
			}
		}
		return pClass;
	}

	protected Type getType(Property forProperty, Class<?> javaType) {
		Type type = getTypeFor(javaType);
		if (type == null) {
			type = getMappingFor(javaType);
		}
		return type;
	}

	protected Map<Property, Object> getValues(Collection<Property> properties,
			Object object) {
		Map<Property, Object> values = new HashMap<Property, Object>();
		for (Property property : properties) {
			values.put(property, property.getValue((object)));
		}
		return values;
	}

	protected void setValues(Map<Property, Object> values, Object object) {
		for (Map.Entry<Property, Object> entry : values.entrySet()) {
			Object value = entry.getValue();
			Property property = entry.getKey();

			property.setValue(object, value);
		}
	}
}
